using System;
using Microsoft.SPOT;


#if NETDUINOGO
namespace NetduinoGo.Modules.PeteBrown
#elif NETDUINO
namespace Netduino.Modules.PeteBrown
#elif GADGETEER
namespace Gadgeteer.Modules.PeteBrown
#endif
{
    public partial class MidiModule
    {
        protected bool IsStatusByte(byte b)
        {
            return (bool)((b & 0x80) != 0);
        }

        protected bool IsDataByte(byte b)
        {
            return (bool)((b & 0x80) == 0);
        }

        /// <summary>
        /// Mode for handling active sense messages. Ignore them, dispatch them like any other
        /// event, or discard them. Active sense is message 0xFE
        /// http://home.roadrunner.com/~jgglatt/tech/midispec/sense.htm
        /// </summary>
        public MidiActiveSenseMode ActiveSenseMode; // { get; set; }

        /// <summary>
        /// Set to true if you want the serial output flushed after each message is sent. Some
        /// boards may require this.
        /// </summary>
        public bool AutoFlushSerialOutput; // { get; set; }


        public MidiChannel FilterToChannel; // { get; set; }

        /// <summary>
        /// Set to true to mirror all traffic from MIDI In to MIDI Out. If the 
        /// app is doing anything intense, I suggest turning this off to help
        /// avoid timing issues.
        /// </summary>
        public bool SoftThru; // { get; set; }

        // MIDI Channel Message Format
        // Status: 1iiinnnn  iii = ID, nnnn = channel
        // http://www.midi.org/techspecs/midimessages.php 
        //
        protected MidiChannel MidiChannelFromRawStatus(byte status)
        {
            if (IsStatusByte(status))
            {
                return (MidiChannel)(status & 0x0F);
            }
            else
                return MidiChannel.ChannelInvalid;
        }

        protected MidiMessageId MidiMessageIdFromRawStatus(byte status)
        {
            if (status < 0x80)
                return MidiMessageId.Invalid;

            if (status == 0xF4 || status == 0xF5 || status == 0xF6 || status == 0xFD)
                return MidiMessageId.Invalid;

            if (status < 0xF0)
                return (MidiMessageId)(status & 0xF0);
            else
                return (MidiMessageId)status;

        }










        /// <summary>
        /// Sends a number of messages at once and waits to flush the output buffer until after the last one. If you
        /// know you need to send a number of simultaneous messages, this is the most efficient way to do it.
        /// </summary>
        /// <param name="messages">Midi Messages to send</param>
        public void SendMultipleMessages(params MidiMessage[] messages)
        {
            byte[] buf;

            int bufferSize = 0;
            foreach (MidiMessage m in messages)
            {
                bufferSize += m.DataByteCount + 1;
            }

            buf = new byte[bufferSize];

            int bufIndex = 0;
            foreach (MidiMessage m in messages)
            {
#if VALIDATE_DATA
                AssertValidMidiChannelForOutput(m.Channel);
#endif

                buf[bufIndex++] = BuildChannelMessageByte(m.Channel, m.ID);

                if (m.DataByteCount >= 1)
                {
#if VALIDATE_DATA
                    AssertValidMidiDataByte(m.DataByte1, "DataByte1");
#endif
                    buf[bufIndex++] = m.DataByte1;
                }

                if (m.DataByteCount == 2)
                {
#if VALIDATE_DATA
                    AssertValidMidiDataByte(m.DataByte2, "DataByte2");
#endif
                    buf[bufIndex++] = m.DataByte2;
                }
            }

            _serial.Write(buf, 0, bufferSize);

            if (AutoFlushSerialOutput)
                _serial.Flush();
        }

        /// <summary>
        /// Send a single MIDI channel message. In most cases, you'll want to use the more specific
        /// SendXYZ method to make it easier for you. However, there's one less function call in the
        /// chain when you use this basic method.
        /// </summary>
        /// <param name="channel">MIDI Channel for the message</param>
        /// <param name="id">Message ID</param>
        /// <param name="data1">First Data Byte</param>
        /// <param name="data2">Second Data Byte</param>
        public void SendChannelMessage(MidiChannel channel, MidiMessageId id, byte data1, byte data2)
        {
#if VALIDATE_DATA
            AssertValidMidiChannelForOutput(channel);
            AssertValidMidiDataByte(data1, "data1");
            AssertValidMidiDataByte(data2, "data2");
#endif

            byte b0 = BuildChannelMessageByte(channel, id);

#if GADGETEER
           // DebugPrint("Sending 2 byte - channel:" + channel + ", id:" + id + ", data1:" + data1 + ", data2:" + data2);
            _serial.Write(b0, data1, data2);
#else
            _serial.Write(new byte[] {b0, data1, data2}, 0, 3);
#endif
            if (AutoFlushSerialOutput)
                _serial.Flush();
        }

        /// <summary>
        /// Send a single MIDI channel message. In most cases, you'll want to use the more specific
        /// SendXYZ method to make it easier for you. However, there's one less function call in the
        /// chain when you use this basic method.
        /// </summary>
        /// <param name="channel">MIDI Channel for the message</param>
        /// <param name="id">Message ID</param>
        /// <param name="data1">First Data Byte</param>
        public void SendChannelMessage(MidiChannel channel, MidiMessageId id, byte data1)
        {
#if VALIDATE_DATA
            AssertValidMidiChannelForOutput(channel);
            AssertValidMidiDataByte(data1, "data1");
#endif
            byte b0 = BuildChannelMessageByte(channel, id);
#if GADGETEER
           // DebugPrint("Sending 1 byte - channel:" + channel + ", id:" + id + ", data1:" + data1);
            _serial.Write(b0, data1);
#else
            _serial.Write(new byte[] { b0, data1 }, 0, 2);
#endif
            if (AutoFlushSerialOutput)
                _serial.Flush();
        }

        /// <summary>
        /// Sends a channel message withuot any data byte.
        /// </summary>
        /// <param name="channel">MIDI Channel for the message</param>
        /// <param name="id">Message ID</param>
        public void SendChannelMessage(MidiChannel channel, MidiMessageId id)
        {
#if VALIDATE_DATA
            AssertValidMidiChannelForOutput(channel);
#endif
            byte b0 = BuildChannelMessageByte(channel, id);
#if GADGETEER
           // DebugPrint("Sending 0 byte - channel:" + channel + ", id:" + id);
            _serial.Write(b0);
#else
            _serial.Write(new byte[] { b0 }, 0, 1);
#endif
            if (AutoFlushSerialOutput)
                _serial.Flush();
        }





        bool _receiveEnabled = false;
        /// <summary>
        /// Enable the MIDI receiver (MIDI IN) and listen on the serial port.
        /// </summary>
        public void EnableReceiver()
        {
            if (!_receiveEnabled)
            {
                // yes, we want to get incoming serial events
                _serial.DataReceived += OnSerialDataReceived;
                _receiveEnabled = true;
#if GADGETEER
                DebugPrint("MIDI Receive Enabled");
#endif
            }
        }

        /// <summary>
        /// Disable the MIDI receiver (MIDI IN) and stop listening on the serial port.
        /// </summary>
        public void DisableReceiver()
        {
            if (_receiveEnabled)
            {
                // no. Cut it out already. We're only going to use MIDI Out
                _serial.DataReceived -= OnSerialDataReceived;
                _receiveEnabled = false;

#if GADGETEER
                DebugPrint("MIDI Receive Disabled");
#endif
            }
        }

        private MidiMessage _currentMessage;
        private bool _currentMessageEmpty = true;

        private enum ParseState
        {
            FirstByte,
            SecondByteOrNextMessage
        }

        private ParseState _currentParseState = ParseState.FirstByte;

        // Parses the buffer of MIDI data. It has to be robust enough to handle situations where it starts 
        // in the middle of a MIDI message, or in cases where messages are lost. That's why id does the 
        // looping checking the top bit of the message byte.
        // Within the MIDI Specification there are two basic types of message byte: 
        //   the status byte and the data byte. The Most Significant Bit of a status byte is always 1, 
        //   the Most Significant Bit of a data byte is always 0. 
        //   The maximum length for a standard MIDI message is three bytes but for some types of MIDI 
        //   message the length can be less.
        // http://midi.songstuff.com/article/midi_message_format
        // Better info:
        // http://www.midi.org/techspecs/midimessages.php
        protected void ParseBuffer(int byteCount)
        {
            int i = 0;

            // skip to next status byte if there's nothing in the partial message
            // This is required for the first time you switch on as you
            // could start receiving bytes in the middle of a MIDI event
            if (_currentMessageEmpty)
                while (i < byteCount && !IsStatusByte(_buffer[i]))
                {
                    i++;
                }


            // loop through all bytes we've received. The buffer can stop and
            // start in the middle of events, so this is a bit of a convoluted
            // bit of processing, byte by byte
            while (i < byteCount)
            {
                //if ((_buffer[i] & 0x80) != 0)
                if (IsStatusByte(_buffer[i]))
                {
                    //if (_currentMessageEmpty)
                    //    _currentMessage = new MidiMessage();
                    _currentMessageEmpty = false;

                    _currentMessage.Channel = MidiChannelFromRawStatus(_buffer[i]);
                    _currentMessage.ID = MidiMessageIdFromRawStatus(_buffer[i]);



#if DEBUG && GADGETEER
                  //  DebugPrint("ParseBuffer Channel: " + _currentMessage.Channel + ", ID: " + _currentMessage.ID.ToString());
#endif

                    i++;

                    _currentParseState = ParseState.FirstByte;

                }
                else
                {
                    // data bytes
                    switch (_currentParseState)
                    {
                        case ParseState.FirstByte:
                            if (_currentMessage.ID == MidiMessageId.RealTimeActiveSensing)
                            {
                                // no data bytes in a Real Time Active Sensing message
                                _currentMessage.DataByteCount = 0;
                                _currentParseState = ParseState.SecondByteOrNextMessage;

                                switch (ActiveSenseMode)
                                {
                                    case MidiActiveSenseMode.AutomaticallyRespond:
                                        // just send the current message
                                        SendChannelMessage(_currentMessage.Channel, MidiMessageId.RealTimeActiveSensing);
                                        break;

                                    case MidiActiveSenseMode.DispatchMessageEvent:
                                        DispatchMessage(_currentMessage);
                                        break;

                                    case MidiActiveSenseMode.Discard:
                                        // do nothing
                                        break;
                                }

                                _currentMessageEmpty = true;
                            }
                            else
                            {
                                // normal message with at least one byte.

                                _currentMessage.DataByte1 = _buffer[i++];
                                _currentMessage.DataByteCount = 1;

#if DEBUG && GADGETEER
                      //      DebugPrint("  Data1: " + _currentMessage.DataByte1);
#endif

                                //if (GetMessageRequiredDataByteCount(_currentMessage.ID) == 1)
                                //    DispatchMessage(_currentMessage);

                                _currentParseState = ParseState.SecondByteOrNextMessage;
                            }
                            break;

                        case ParseState.SecondByteOrNextMessage:
                            _currentMessage.DataByte2 = _buffer[i++];
                            _currentMessage.DataByteCount = 2;

#if DEBUG && GADGETEER
                       //     DebugPrint("  Data2: " + _currentMessage.DataByte2);
#endif

                            //if (GetMessageRequiredDataByteCount(_currentMessage.ID) == 2)
                            _currentMessageEmpty = true;
                            DispatchMessage(_currentMessage);

                            break;

                        default:
                            // sum'in not right
                            i++;
                            break;

                    }
                }
            }
        }





        private void AssertValidMidiChannelForOutput(MidiChannel channel)
        {
            if (channel == MidiChannel.ChannelInvalid || channel == MidiChannel.ChannelOmni)
                throw new ArgumentException("Invalid MIDI channel '" + channel + "'.", "channel");
        }

        private void AssertValidMidiDataByte(byte b, string parameterName)
        {
            if (!IsDataByte(b))
                throw new ArgumentException("MIDI data bytes must be between 0 and 127. Value was '" + b + "'", parameterName);
        }

        // I really wish we had macro expansion in C# for things like this.
        private byte BuildChannelMessageByte(MidiChannel channel, MidiMessageId id)
        {
            return (byte)((byte)id | (byte)channel);
        }


        /// <summary>
        /// Sends a single "Note On" message. Use this to trigger playing a note.
        /// </summary>
        /// <param name="channel">MIDI Channel for the message</param>
        /// <param name="noteNumber">The MIDI Note number between 0 and 127</param>
        /// <param name="velocity">A value between 0 and 127. A velocity of 0 effectively makes this a "Note Off"</param>
        public void SendNoteOn(MidiChannel channel, byte noteNumber, byte velocity)
        {
#if VALIDATE_DATA
            AssertValidMidiChannelForOutput(channel);
            AssertValidMidiDataByte(noteNumber, "noteNumber");
            AssertValidMidiDataByte(velocity, "velocity");
#endif

            SendChannelMessage(channel, MidiMessageId.ChannelNoteOn, noteNumber, velocity);
        }

        /// <summary>
        /// Sends a single "Note Off" message. Use this to stop playing a note. An alternate 
        /// approach (used in a lot of MIDI hardware) is to send a Note On message with a velocity of zero.
        /// </summary>
        /// <param name="channel">MIDI Channel for the message</param>
        /// <param name="noteNumber">The MIDI Note number between 0 and 127</param>
        /// <param name="velocity">A value between 0 and 127</param>
        public void SendNoteOff(MidiChannel channel, byte noteNumber, byte velocity)
        {
#if VALIDATE_DATA
            AssertValidMidiChannelForOutput(channel);
            AssertValidMidiDataByte(noteNumber, "noteNumber");
            AssertValidMidiDataByte(velocity, "velocity");
#endif
            //SendNoteOn(channel, noteNumber, 0);
            SendChannelMessage(channel, MidiMessageId.ChannelNoteOff, noteNumber, velocity);
        }

        /// <summary>
        /// Sends a polyphonic aftertouch message for a single note.
        /// </summary>
        /// <param name="channel">MIDI Channel for the message</param>
        /// <param name="noteNumber">The MIDI Note number between 0 and 127</param>
        /// <param name="pressure">The aftertouch value between 0 and 127</param>
        public void SendPolyphonicAftertouch(MidiChannel channel, byte noteNumber, byte pressure)
        {
#if VALIDATE_DATA
            AssertValidMidiChannelForOutput(channel);
            AssertValidMidiDataByte(noteNumber, "noteNumber");
            AssertValidMidiDataByte(pressure, "pressure");
#endif

            SendChannelMessage(channel, MidiMessageId.ChannelPolyPressure, noteNumber, pressure);
        }

        /// <summary>
        /// Sends a control change message. The actual control number to use will depend upon
        /// your target device. Look at their MIDI implementation chart for details.
        /// </summary>
        /// <param name="channel">MIDI Channel for the message</param>
        /// <param name="control">The control number (0..127)</param>
        /// <param name="value">The control value (0..127)</param>
        public void SendControlChange(MidiChannel channel, byte control, byte value)
        {
#if VALIDATE_DATA
            AssertValidMidiChannelForOutput(channel);
            AssertValidMidiDataByte(control, "control");
            AssertValidMidiDataByte(value, "value");
#endif
            SendChannelMessage(channel, MidiMessageId.ChannelControlChange, control, value);
        }

        /// <summary>
        /// Sends a program change message to tell the device to change the current patch/sound/program.
        /// </summary>
        /// <param name="channel">MIDI Channel for the message</param>
        /// <param name="program">The program number (0..127)</param>
        public void SendProgramChange(MidiChannel channel, byte program)
        {
#if VALIDATE_DATA
            AssertValidMidiChannelForOutput(channel);
            AssertValidMidiDataByte(program, "program");
#endif
            SendChannelMessage(channel, MidiMessageId.ChannelProgramChange, program);
        }

        /// <summary>
        /// Sends channel-wide aftertouch. This is more common than polyphonic aftertouch in older equipment.
        /// </summary>
        /// <param name="channel">MIDI Channel for the message</param>
        /// <param name="pressure">The aftertouch value between 0 and 127</param>
        public void SendChannelAftertouch(MidiChannel channel, byte pressure)
        {
#if VALIDATE_DATA
            AssertValidMidiChannelForOutput(channel);
            AssertValidMidiDataByte(pressure, "pressure");
#endif
            SendChannelMessage(channel, MidiMessageId.ChannelPressure, pressure);
        }

        /// <summary>
        /// Pitch wheel change is a 14 bit value with center being 0x2000
        /// Any value in the 15th and 16th bits is discarded. The interpretation of the value
        /// and mapping it to an actual pitch range is up to the target device.
        /// </summary>
        /// <param name="channel">MIDI Channel for the message</param>
        /// <param name="value">A 14 bit pitch wheel change value. Center is 0x2000</param>
        public void SendPitchWheelChange(MidiChannel channel, Int16 value)
        {
#if VALIDATE_DATA
            AssertValidMidiChannelForOutput(channel);
#endif
            // bottom 7 bits
            byte lsb = (byte)(value & 0x7F);

            // top 7 bits
            byte msb = (byte)((value & 0x3F80) >> 7);

            SendChannelMessage(channel, MidiMessageId.ChannelPitchWheel, lsb, msb);
        }

        /// <summary>
        /// Turn all sound off.
        /// </summary>
        /// <param name="channel">MIDI Channel for the message</param>
        public void SendChannelAllSoundOff(MidiChannel channel)
        {
#if VALIDATE_DATA
            AssertValidMidiChannelForOutput(channel);
#endif
            SendChannelMessage(channel, MidiMessageId.ChannelControlChange, (byte)120, (byte)0);
        }

        /// <summary>
        /// Reset all controllers
        /// </summary>
        /// <param name="channel">MIDI Channel for the message</param>
        public void SendChannelResetAllControllers(MidiChannel channel)
        {
#if VALIDATE_DATA
            AssertValidMidiChannelForOutput(channel);
#endif
            SendChannelMessage(channel, MidiMessageId.ChannelControlChange, (byte)121, (byte)0);
        }

        /// <summary>
        /// Tell a device whether or not local control should be enabled for hte channel.
        /// </summary>
        /// <param name="channel">MIDI Channel for the message</param>
        /// <param name="localControlEnabled">True if local control is enabled</param>
        public void SendChannelLocalControl(MidiChannel channel, bool localControlEnabled)
        {
#if VALIDATE_DATA
            AssertValidMidiChannelForOutput(channel);
#endif
            SendChannelMessage(channel, MidiMessageId.ChannelControlChange, (byte)122, (byte)(localControlEnabled ? 127 : 0));
        }

        /// <summary>
        /// Send an "all notes off" message for the channel.
        /// </summary>
        /// <param name="channel">MIDI Channel for the message</param>
        public void SendChannelAllNotesOff(MidiChannel channel)
        {
#if VALIDATE_DATA
            AssertValidMidiChannelForOutput(channel);
#endif
            SendChannelMessage(channel, MidiMessageId.ChannelControlChange, (byte)123, (byte)0);
        }

        /// <summary>
        /// Sends an Omni mode message.
        /// </summary>
        /// <param name="channel">MIDI Channel for the message</param>
        /// <param name="omniModeEnabled">True if omni mode is to be enabled for the channel</param>
        public void SendChannelOmniMode(MidiChannel channel, bool omniModeEnabled)
        {
#if VALIDATE_DATA
            AssertValidMidiChannelForOutput(channel);
#endif
            SendChannelMessage(channel, MidiMessageId.ChannelControlChange, (byte)(omniModeEnabled ? 125 : 124), (byte)0);
        }

        /// <summary>
        /// Tells the device to go into mono mode
        /// </summary>
        /// <param name="channel">MIDI Channel for the message</param>
        /// <param name="numberOfChannels">The number of mono channels</param>
        public void SendChannelMonoModeOn(MidiChannel channel, byte numberOfChannels)
        {
#if VALIDATE_DATA
            AssertValidMidiChannelForOutput(channel);
#endif
            SendChannelMessage(channel, MidiMessageId.ChannelControlChange, (byte)126, numberOfChannels);
        }

        /// <summary>
        /// Tells the device to go into poly mode.
        /// </summary>
        /// <param name="channel">MIDI Channel for the message</param>
        public void SendChannelPolyModeOn(MidiChannel channel)
        {
#if VALIDATE_DATA
            AssertValidMidiChannelForOutput(channel);
#endif
            SendChannelMessage(channel, MidiMessageId.ChannelControlChange, (byte)127, (byte)0);
        }
    }
}
